1use std::{
2 collections::{HashMap, VecDeque},
3 sync::{
4 Arc, Mutex, Weak,
5 atomic::{AtomicU64, Ordering},
6 },
7 thread,
8 time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_error, pp_info};
12use ffmpeg_next as ffmpeg;
13use thiserror::Error as ThisError;
14
15use crate::{
16 buffer::MediaBuffer,
17 bus::{Bus, BusEvent},
18 control::{ControlMsg, ControlReceiver, drain_control},
19 element::{Element, ElementType, Sink, Source, SourceElement, element_pp_log},
20 error::Result,
21 pad::SrcPad,
22 schedule::ActiveTimeline,
23};
24
25const TICK_INTERVAL: Duration = Duration::from_millis(20);
30
31#[derive(Debug, ThisError)]
34pub enum AudioMixerError {
35 #[error("ffmpeg error: {0}")]
36 Ffmpeg(#[from] ffmpeg_next::Error),
37
38 #[error("AudioMixer doesn't support seeking a live mix")]
39 SeekUnsupported,
40
41 #[error("AudioMixer inputs only accept Audio or Eos buffers, got {0}")]
42 UnsupportedBuffer(&'static str),
43}
44
45#[derive(Debug, Clone, Copy)]
50pub struct AudioMixerOptions {
51 pub sample_rate: u32,
52 pub channels: u16,
53}
54
55struct InputBuffer {
62 id: u64,
65 resampler: Option<ffmpeg::software::resampling::Context>,
66 samples: VecDeque<f32>,
67 eos: bool,
73}
74
75impl InputBuffer {
76 fn push(
77 &mut self,
78 frame: &ffmpeg::frame::Audio,
79 target_format: ffmpeg::format::Sample,
80 target_layout: ffmpeg::ChannelLayout,
81 target_rate: u32,
82 ) -> std::result::Result<(), AudioMixerError> {
83 let resampler = match &mut self.resampler {
84 Some(resampler) => resampler,
85 None => {
86 let resampler = ffmpeg::software::resampling::Context::get(
87 frame.format(),
88 frame.channel_layout(),
89 frame.rate(),
90 target_format,
91 target_layout,
92 target_rate,
93 )?;
94 self.resampler.insert(resampler)
95 }
96 };
97 let mut output = ffmpeg::frame::Audio::empty();
98 resampler.run(frame, &mut output)?;
99 let samples = output.samples();
110 let channels = target_layout.channels() as usize;
111 let bytes = &output.data(0)[..samples * channels * 4];
112 let interleaved =
113 unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f32, bytes.len() / 4) };
114 self.samples.extend(interleaved.iter().copied());
115 Ok(())
116 }
117}
118
119struct MixerShared {
125 inputs: Mutex<HashMap<Arc<str>, InputBuffer>>,
126 next_input_id: AtomicU64,
129}
130
131#[derive(Clone)]
140pub struct MixerHandle {
141 shared: Weak<MixerShared>,
142 sample_rate: u32,
143 format: ffmpeg::format::Sample,
144 channel_layout: ffmpeg::ChannelLayout,
145}
146
147impl MixerHandle {
148 pub fn add_source(&self, name: impl Into<String>) -> Option<Box<dyn Sink>> {
163 let shared = self.shared.upgrade()?;
164 let name: Arc<str> = name.into().into();
165 let id = shared.next_input_id.fetch_add(1, Ordering::Relaxed);
166 shared.inputs.lock().unwrap().insert(
167 name.clone(),
168 InputBuffer {
169 id,
170 resampler: None,
171 samples: VecDeque::new(),
172 eos: false,
173 },
174 );
175 Some(Box::new(MixerInputSink {
176 name: name.clone(),
177 id,
178 pp_log: element_pp_log(ElementType::AudioMixer, &name, None),
179 shared: self.shared.clone(),
180 target_format: self.format,
181 target_layout: self.channel_layout,
182 target_rate: self.sample_rate,
183 }))
184 }
185
186 pub fn remove_source(&self, name: &str) {
190 if let Some(shared) = self.shared.upgrade() {
191 shared.inputs.lock().unwrap().remove(name);
192 }
193 }
194
195 pub fn source_count(&self) -> usize {
196 self.shared
197 .upgrade()
198 .map(|shared| shared.inputs.lock().unwrap().len())
199 .unwrap_or(0)
200 }
201}
202
203pub struct MixerInputSink {
211 pp_log: PpLog,
212 name: Arc<str>,
213 id: u64,
217 shared: Weak<MixerShared>,
218 target_format: ffmpeg::format::Sample,
219 target_layout: ffmpeg::ChannelLayout,
220 target_rate: u32,
221}
222
223unsafe impl Send for MixerInputSink {}
229
230impl Element for MixerInputSink {
231 fn name(&self) -> Arc<str> {
232 self.name.clone()
233 }
234
235 fn element_type(&self) -> ElementType {
236 ElementType::AudioMixer
237 }
238
239 fn pp_log(&self) -> &PpLog {
240 &self.pp_log
241 }
242
243 fn pp_log_mut(&mut self) -> &mut PpLog {
244 &mut self.pp_log
245 }
246}
247
248impl Sink for MixerInputSink {
249 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
250 let Some(shared) = self.shared.upgrade() else {
251 return Ok(()); };
253 match buf {
254 MediaBuffer::Audio(frame) => {
255 let mut inputs = shared.inputs.lock().unwrap();
256 if let Some(input) = inputs.get_mut(&self.name)
257 && input.id == self.id
258 {
259 input.push(
260 &frame,
261 self.target_format,
262 self.target_layout,
263 self.target_rate,
264 )?;
265 }
266 }
270 MediaBuffer::Eos => {
271 let mut inputs = shared.inputs.lock().unwrap();
272 if let Some(input) = inputs.get_mut(&self.name)
273 && input.id == self.id
274 {
275 input.eos = true;
276 }
277 }
278 other => {
279 pp_error!(self, "unsupported buffer: expected Audio or Eos");
280 return Err(AudioMixerError::UnsupportedBuffer(other.kind()).into());
281 }
282 }
283 Ok(())
284 }
285
286 fn control(&mut self, msg: ControlMsg) -> Result<()> {
302 if msg == ControlMsg::Stop
303 && let Some(shared) = self.shared.upgrade()
304 {
305 let mut inputs = shared.inputs.lock().unwrap();
306 if inputs
307 .get(&self.name)
308 .is_some_and(|input| input.id == self.id)
309 {
310 inputs.remove(&self.name);
311 }
312 }
313 Ok(())
314 }
315}
316
317pub struct AudioMixer {
354 pp_log: PpLog,
355 name: Arc<str>,
356 shared: Arc<MixerShared>,
357 pad: SrcPad,
358 sample_rate: u32,
359 format: ffmpeg::format::Sample,
360 channel_layout: ffmpeg::ChannelLayout,
361 channels: u16,
362 samples_emitted: i64,
365}
366
367unsafe impl Send for AudioMixer {}
371
372impl AudioMixer {
373 pub fn new(name: impl Into<String>, options: AudioMixerOptions) -> (Self, MixerHandle) {
381 let name: Arc<str> = name.into().into();
382 let pp_log = element_pp_log(ElementType::AudioMixer, &name, None);
383 pp_info!(
384 pp_log: &pp_log,
385 "created: {}Hz, {} channel(s)",
386 options.sample_rate,
387 options.channels
388 );
389 let format = ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed);
390 let channel_layout = ffmpeg::ChannelLayout::default(options.channels as i32);
391 let shared = Arc::new(MixerShared {
392 inputs: Mutex::new(HashMap::new()),
393 next_input_id: AtomicU64::new(0),
394 });
395 let pad = SrcPad::new(format!("{name}_src"));
396 (
397 Self {
398 name: name.clone(),
399 pp_log,
400 shared: shared.clone(),
401 pad,
402 sample_rate: options.sample_rate,
403 format,
404 channel_layout,
405 channels: options.channels,
406 samples_emitted: 0,
407 },
408 MixerHandle {
409 shared: Arc::downgrade(&shared),
410 sample_rate: options.sample_rate,
411 format,
412 channel_layout,
413 },
414 )
415 }
416
417 pub fn time_base(&self) -> ffmpeg::Rational {
419 ffmpeg::Rational::new(1, self.sample_rate as i32)
420 }
421
422 fn mix_tick(&mut self, elapsed: Duration, bus: &Bus) {
434 let channels = self.channels as usize;
435 let expected = (elapsed.as_secs_f64() * self.sample_rate as f64) as i64;
436 let needed = (expected - self.samples_emitted).max(0) as usize;
437 if needed == 0 {
438 return;
439 }
440 let mut mixed = vec![0f32; needed * channels];
441 {
442 let mut inputs = self.shared.inputs.lock().unwrap();
443 inputs.retain(|_, input| !(input.eos && input.samples.is_empty()));
444 for input in inputs.values_mut() {
445 let take = mixed.len().min(input.samples.len());
446 for (slot, sample) in mixed.iter_mut().zip(input.samples.iter()) {
447 *slot += *sample;
448 }
449 input.samples.drain(0..take);
450 }
451 }
452 for sample in &mut mixed {
453 *sample = sample.clamp(-1.0, 1.0);
454 }
455
456 let mut frame = ffmpeg::frame::Audio::new(self.format, needed, self.channel_layout);
457 frame.set_rate(self.sample_rate);
458 let bytes = unsafe {
459 std::slice::from_raw_parts(mixed.as_ptr() as *const u8, std::mem::size_of_val(&*mixed))
460 };
461 frame.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
468 frame.set_pts(Some(self.samples_emitted));
469 self.samples_emitted += needed as i64;
470
471 if let Err(error) = self.pad.push(MediaBuffer::Audio(Arc::new(frame))) {
472 bus.post(
473 &self.pp_log,
474 BusEvent::Error {
475 element_type: ElementType::AudioMixer,
476 name: self.name.clone(),
477 error,
478 },
479 );
480 }
481 }
482}
483
484impl Element for AudioMixer {
485 fn name(&self) -> Arc<str> {
486 self.name.clone()
487 }
488
489 fn element_type(&self) -> ElementType {
490 ElementType::AudioMixer
491 }
492
493 fn pp_log(&self) -> &PpLog {
494 &self.pp_log
495 }
496
497 fn pp_log_mut(&mut self) -> &mut PpLog {
498 &mut self.pp_log
499 }
500}
501
502impl Source for AudioMixer {
503 fn src_pads(&mut self) -> &mut [SrcPad] {
504 std::slice::from_mut(&mut self.pad)
505 }
506}
507
508impl SourceElement for AudioMixer {
509 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
510 pp_info!(self, "started");
511 let mut timeline = ActiveTimeline::new(Instant::now());
512 loop {
513 let outcome = drain_control(control, self, bus)?;
514 if outcome.stopped {
515 pp_info!(self, "stopped");
516 return Ok(());
517 }
518 timeline.account_pause(outcome.paused_for);
519 thread::sleep(TICK_INTERVAL);
520 self.mix_tick(timeline.elapsed(Instant::now()), bus);
521 }
522 }
523
524 fn seek(&mut self, _target: Duration) -> Result<Duration> {
525 Err(AudioMixerError::SeekUnsupported.into())
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use std::sync::{
532 Mutex as StdMutex,
533 atomic::{AtomicBool, Ordering},
534 };
535
536 use crate::pp_log::PpLog;
537
538 use super::*;
539 use crate::pipeline::Pipeline;
540
541 fn constant_frame(value: f32, samples: usize, rate: u32) -> ffmpeg::frame::Audio {
542 let mut frame = ffmpeg::frame::Audio::new(
543 ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
544 samples,
545 ffmpeg::ChannelLayout::default(1),
546 );
547 frame.set_rate(rate);
548 frame.plane_mut::<f32>(0).fill(value);
549 frame
550 }
551
552 struct RecordingSink {
553 pp_log: PpLog,
554 seen: Arc<StdMutex<Vec<f32>>>,
555 }
556
557 impl Element for RecordingSink {
558 fn name(&self) -> Arc<str> {
559 "recorder".into()
560 }
561 fn element_type(&self) -> ElementType {
562 ElementType::Other
563 }
564 fn pp_log(&self) -> &PpLog {
565 &self.pp_log
566 }
567 fn pp_log_mut(&mut self) -> &mut PpLog {
568 &mut self.pp_log
569 }
570 }
571
572 impl Sink for RecordingSink {
573 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
574 if let MediaBuffer::Audio(frame) = buf
575 && frame.samples() > 0
576 {
577 self.seen.lock().unwrap().push(frame.plane::<f32>(0)[0]);
578 }
579 Ok(())
580 }
581 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
582 Ok(())
583 }
584 }
585
586 fn constant_stereo_frame(
587 left: f32,
588 right: f32,
589 samples: usize,
590 rate: u32,
591 ) -> ffmpeg::frame::Audio {
592 let mut frame = ffmpeg::frame::Audio::new(
593 ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
594 samples,
595 ffmpeg::ChannelLayout::default(2),
596 );
597 frame.set_rate(rate);
598 let bytes = frame.data_mut(0);
599 let floats =
600 unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut f32, samples * 2) };
601 for pair in floats.chunks_mut(2) {
602 pair[0] = left;
603 pair[1] = right;
604 }
605 frame
606 }
607
608 struct StereoRecordingSink {
609 pp_log: PpLog,
610 seen: Arc<StdMutex<Vec<(f32, f32)>>>,
611 }
612
613 impl Element for StereoRecordingSink {
614 fn name(&self) -> Arc<str> {
615 "stereo-recorder".into()
616 }
617 fn element_type(&self) -> ElementType {
618 ElementType::Other
619 }
620 fn pp_log(&self) -> &PpLog {
621 &self.pp_log
622 }
623 fn pp_log_mut(&mut self) -> &mut PpLog {
624 &mut self.pp_log
625 }
626 }
627
628 impl Sink for StereoRecordingSink {
629 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
630 if let MediaBuffer::Audio(frame) = buf
631 && frame.samples() > 0
632 {
633 let samples = frame.samples();
641 let bytes = &frame.data(0)[..samples * 2 * 4];
642 let floats = unsafe {
643 std::slice::from_raw_parts(bytes.as_ptr() as *const f32, samples * 2)
644 };
645 self.seen.lock().unwrap().push((floats[0], floats[1]));
646 }
647 Ok(())
648 }
649 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
650 Ok(())
651 }
652 }
653
654 #[test]
662 fn mixes_stereo_sources_without_channel_corruption() {
663 let (mixer, handle) = AudioMixer::new(
664 "mixer",
665 AudioMixerOptions {
666 sample_rate: 48000,
667 channels: 2,
668 },
669 );
670 let seen = Arc::new(StdMutex::new(Vec::new()));
671 let sink = StereoRecordingSink {
672 seen: seen.clone(),
673 pp_log: element_pp_log(ElementType::Other, "stereo-recorder", None),
674 };
675
676 let pipeline = Pipeline::new("mixer-stereo-test", mixer, |source, ctx| {
677 let branch = ctx.branch().to(Box::new(sink))?;
678 ctx.attach(source, 0, branch)?;
679 Ok(())
680 })
681 .expect("test pipeline wiring must succeed");
682 pipeline.run();
683
684 let mut input_a = handle.add_source("a").expect("mixer still alive");
685 let mut input_b = handle.add_source("b").expect("mixer still alive");
686
687 let stop = Arc::new(AtomicBool::new(false));
688 let feeder_stop = stop.clone();
689 let feeder = std::thread::spawn(move || {
690 while !feeder_stop.load(Ordering::Relaxed) {
691 let _ = input_a.consume(MediaBuffer::Audio(Arc::new(constant_stereo_frame(
692 0.2, -0.1, 480, 48000,
693 ))));
694 let _ = input_b.consume(MediaBuffer::Audio(Arc::new(constant_stereo_frame(
695 0.1, -0.2, 480, 48000,
696 ))));
697 std::thread::sleep(Duration::from_millis(10));
698 }
699 });
700
701 std::thread::sleep(Duration::from_millis(300));
702 stop.store(true, Ordering::Relaxed);
703 feeder.join().unwrap();
704 pipeline.stop();
705 pipeline.bus().log_events();
706
707 let seen = seen.lock().unwrap();
708 assert!(
709 seen.len() > 5,
710 "expected several mixed frames, got {seen:?}"
711 );
712 let steady = &seen[3..seen.len() - 2];
713 for &(left, right) in steady {
714 assert!(
715 (left - 0.3).abs() < 0.01,
716 "expected left channel ~0.3, got {left} in {seen:?}"
717 );
718 assert!(
719 (right - -0.3).abs() < 0.01,
720 "expected right channel ~-0.3, got {right} in {seen:?}"
721 );
722 }
723 }
724
725 #[test]
731 fn mixes_two_sources_and_hard_clips() {
732 let (mixer, handle) = AudioMixer::new(
733 "mixer",
734 AudioMixerOptions {
735 sample_rate: 48000,
736 channels: 1,
737 },
738 );
739 let seen = Arc::new(StdMutex::new(Vec::new()));
740 let sink = RecordingSink {
741 seen: seen.clone(),
742 pp_log: element_pp_log(ElementType::Other, "recorder", None),
743 };
744
745 let pipeline = Pipeline::new("mixer-test", mixer, |source, ctx| {
746 let branch = ctx.branch().to(Box::new(sink))?;
747 ctx.attach(source, 0, branch)?;
748 Ok(())
749 })
750 .expect("test pipeline wiring must succeed");
751 pipeline.run();
752
753 let mut input_a = handle.add_source("a").expect("mixer still alive");
754 let mut input_b = handle.add_source("b").expect("mixer still alive");
755 assert_eq!(handle.source_count(), 2);
756
757 let stop = Arc::new(AtomicBool::new(false));
758 let feeder_stop = stop.clone();
759 let feeder = std::thread::spawn(move || {
760 while !feeder_stop.load(Ordering::Relaxed) {
761 let _ = input_a.consume(MediaBuffer::Audio(Arc::new(constant_frame(
762 0.6, 480, 48000,
763 ))));
764 let _ = input_b.consume(MediaBuffer::Audio(Arc::new(constant_frame(
765 0.6, 480, 48000,
766 ))));
767 std::thread::sleep(Duration::from_millis(10));
768 }
769 });
770
771 std::thread::sleep(Duration::from_millis(300));
772 stop.store(true, Ordering::Relaxed);
773 feeder.join().unwrap();
774 pipeline.stop();
775 pipeline.bus().log_events();
776
777 let seen = seen.lock().unwrap();
778 assert!(
779 seen.len() > 5,
780 "expected several mixed frames, got {seen:?}"
781 );
782 let steady = &seen[3..seen.len() - 2];
787 for &value in steady {
788 assert!(
789 (value - 1.0).abs() < 0.01,
790 "expected hard-clipped ~1.0, got {value} in {seen:?}"
791 );
792 }
793 }
794
795 #[test]
796 fn removed_source_stops_contributing() {
797 let (mixer, handle) = AudioMixer::new(
798 "mixer",
799 AudioMixerOptions {
800 sample_rate: 48000,
801 channels: 1,
802 },
803 );
804 let seen = Arc::new(StdMutex::new(Vec::new()));
805 let sink = RecordingSink {
806 seen: seen.clone(),
807 pp_log: element_pp_log(ElementType::Other, "recorder", None),
808 };
809 let pipeline = Pipeline::new("mixer-test-2", mixer, |source, ctx| {
810 let branch = ctx.branch().to(Box::new(sink))?;
811 ctx.attach(source, 0, branch)?;
812 Ok(())
813 })
814 .expect("test pipeline wiring must succeed");
815 pipeline.run();
816
817 let mut input_a = handle.add_source("a").unwrap();
818 input_a
819 .consume(MediaBuffer::Audio(Arc::new(constant_frame(
820 0.5, 480, 48000,
821 ))))
822 .unwrap();
823 std::thread::sleep(Duration::from_millis(100));
824 handle.remove_source("a");
825 assert_eq!(handle.source_count(), 0);
826 seen.lock().unwrap().clear();
827
828 std::thread::sleep(Duration::from_millis(100));
829 pipeline.stop();
830 pipeline.bus().log_events();
831
832 assert!(
833 seen.lock().unwrap().iter().all(|&v| v == 0.0),
834 "removed source must not keep contributing: {:?}",
835 *seen.lock().unwrap()
836 );
837 }
838
839 #[test]
847 fn stopped_source_is_removed_without_an_explicit_remove_source_call() {
848 let (mixer, handle) = AudioMixer::new(
849 "mixer",
850 AudioMixerOptions {
851 sample_rate: 48000,
852 channels: 1,
853 },
854 );
855 let seen = Arc::new(StdMutex::new(Vec::new()));
856 let sink = RecordingSink {
857 seen: seen.clone(),
858 pp_log: element_pp_log(ElementType::Other, "recorder", None),
859 };
860 let pipeline = Pipeline::new("mixer-test-3", mixer, |source, ctx| {
861 let branch = ctx.branch().to(Box::new(sink))?;
862 ctx.attach(source, 0, branch)?;
863 Ok(())
864 })
865 .expect("test pipeline wiring must succeed");
866 pipeline.run();
867
868 let mut input_a = handle.add_source("a").unwrap();
869 input_a
870 .consume(MediaBuffer::Audio(Arc::new(constant_frame(
871 0.5, 480, 48000,
872 ))))
873 .unwrap();
874 assert_eq!(handle.source_count(), 1);
875
876 input_a.control(ControlMsg::Stop).unwrap();
880
881 assert_eq!(
882 handle.source_count(),
883 0,
884 "Stop should remove the input immediately, same as remove_source"
885 );
886
887 pipeline.stop();
888 pipeline.bus().log_events();
889 }
890
891 #[test]
897 fn replacing_an_input_by_name_invalidates_the_stale_sink() {
898 let (_mixer, handle) = AudioMixer::new(
899 "mixer",
900 AudioMixerOptions {
901 sample_rate: 48000,
902 channels: 1,
903 },
904 );
905 let mut stale = handle.add_source("mic").expect("mixer still alive");
906 let mut current = handle.add_source("mic").expect("mixer still alive");
907 assert_eq!(handle.source_count(), 1);
908
909 stale
910 .consume(MediaBuffer::Audio(Arc::new(constant_frame(
911 0.75, 480, 48000,
912 ))))
913 .unwrap();
914 stale.consume(MediaBuffer::Eos).unwrap();
915 stale.control(ControlMsg::Stop).unwrap();
916
917 assert_eq!(
918 handle.source_count(),
919 1,
920 "a stale sink's Stop must not remove its replacement"
921 );
922 let shared = handle.shared.upgrade().expect("mixer still alive");
923 {
924 let inputs = shared.inputs.lock().unwrap();
925 let input = inputs.get("mic").expect("replacement remains registered");
926 assert!(
927 input.resampler.is_none() && input.samples.is_empty(),
928 "stale audio must not enter the replacement buffer"
929 );
930 assert!(!input.eos, "stale Eos must not mark the replacement ended");
931 }
932
933 current
936 .consume(MediaBuffer::Audio(Arc::new(constant_frame(
937 0.25, 480, 48000,
938 ))))
939 .unwrap();
940 current.consume(MediaBuffer::Eos).unwrap();
941 {
942 let inputs = shared.inputs.lock().unwrap();
943 let input = inputs.get("mic").expect("replacement remains registered");
944 assert!(input.resampler.is_some(), "current audio was not accepted");
945 assert!(input.eos, "current Eos was not accepted");
946 }
947
948 current.control(ControlMsg::Stop).unwrap();
949 assert_eq!(handle.source_count(), 0);
950 }
951
952 #[test]
958 fn rejects_buffers_that_are_neither_audio_nor_eos() {
959 let (mixer, handle) = AudioMixer::new(
960 "mixer",
961 AudioMixerOptions {
962 sample_rate: 48000,
963 channels: 2,
964 },
965 );
966 let mut input = handle.add_source("a").expect("mixer still alive");
967
968 let error = input
969 .consume(MediaBuffer::Packet(Arc::new(ffmpeg::Packet::empty())))
970 .expect_err("a Packet buffer must be rejected, not silently dropped");
971 assert!(
972 matches!(
973 error,
974 crate::error::Error::AudioMixerError(AudioMixerError::UnsupportedBuffer("Packet"))
975 ),
976 "unexpected error: {error:?}"
977 );
978
979 drop(mixer);
980 }
981}